package com.ruoyi.matrix.util

import cn.hutool.core.codec.Base64
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.logging.HttpLoggingInterceptor
import org.slf4j.LoggerFactory
import java.net.InetAddress
import java.net.InetSocketAddress
import java.util.concurrent.TimeUnit

interface CookieJarManager : CookieJar {
    fun getCookie(domain: String, name: String): String?
    fun getCookies(domain: String): Map<String, String>
}

// 存储在内存中的cookie管理器
// @todo okhttp的cookie jar似乎不能处理302
class MemoryCookieJar : CookieJarManager {
    private val _cookies: MutableMap<String, MutableMap<String, Cookie>> = hashMapOf()

    override fun loadForRequest(url: HttpUrl): List<Cookie> {
        val cookiesInDomain = this._cookies.filter { (domain, cookies) ->
            val domains = arrayOf(url.host, url.topPrivateDomain(), "." + url.topPrivateDomain())
            domain in domains
        }

        if (cookiesInDomain.isEmpty()) return listOf()

        val validCookies = mutableListOf<Cookie>()
        cookiesInDomain.values.forEach { cookeis ->
            validCookies.addAll(cookeis.filter { (name, cookie) ->
                if (cookie.expiresAt < System.currentTimeMillis()) return@filter false
                if (cookie.path == "/") return@filter true

                val urlPath = url.pathSegments.joinToString(separator = "/", prefix = "/")
                urlPath.startsWith(cookie.path)
            }.values)
        }

        return validCookies
    }

    override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
        for (cookie in cookies) {
            this._cookies.putIfAbsent(cookie.domain, mutableMapOf())
            this._cookies[cookie.domain]!![cookie.name] = cookie
        }
    }

    override fun getCookie(domain: String, name: String): String? {
        if (_cookies.containsKey(domain)) {
            val cookie = _cookies[domain]!!.getOrDefault(name, null) ?: return null
            return cookie.value
        }
        return null
    }

    override fun getCookies(domain: String): Map<String, String> {
        if (_cookies.containsKey(domain)) {
            return _cookies[domain]!!.map { it.key to it.value.value }.toMap()
        }
        return mapOf()
    }

    fun clear() = this._cookies.clear()
}

abstract class NetworkInterceptor : Interceptor {
    override fun toString() = "302请求也会拦截的拦截器"
}

open class HttpManager(var client: OkHttpClient = defaultClient) {
    companion object {
        val MediaTypeHtml = "text/html".toMediaType()
        val MediaTypeJSON = "application/json; charset=utf-8".toMediaType()
        val MediaTypeFormUrlEncoded = "application/x-www-form-urlencoded".toMediaType()
        private val typeMapping = mapOf(
            "http" to java.net.Proxy.Type.HTTP,
            "https" to java.net.Proxy.Type.HTTP,
            "socks" to java.net.Proxy.Type.SOCKS,
            "socks5" to java.net.Proxy.Type.SOCKS,
        )

        @PublishedApi
        internal val logger = LoggerFactory.getLogger(HttpManager::class.java)

        // 默认的client，共享连接池
        val defaultClient = OkHttpClient.Builder()
            .protocols(listOf(Protocol.HTTP_1_1))
            .connectTimeout(5, TimeUnit.SECONDS)
            .readTimeout(10, TimeUnit.SECONDS)
            .build()

        fun getClient(
            timeout: Long = 10,
            logLevel: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.NONE,
            interceptors: List<Interceptor> = listOf(),
            cookieJar: CookieJarManager = MemoryCookieJar(),
            proxy: Proxy? = null
        ): OkHttpClient {

            val logInterceptor = HttpLoggingInterceptor { this.logger.info(it) }
            logInterceptor.level = logLevel

            val clientBuilder = this.defaultClient
                .newBuilder()
                .protocols(listOf(Protocol.HTTP_1_1))
                .addNetworkInterceptor(logInterceptor)
                .cookieJar(cookieJar)
                .connectTimeout(timeout, TimeUnit.SECONDS)
                .readTimeout(timeout, TimeUnit.SECONDS)
                .writeTimeout(timeout, TimeUnit.SECONDS)

            for (interceptor in interceptors) {
                if (interceptor is NetworkInterceptor) {
                    clientBuilder.addNetworkInterceptor(interceptor)
                } else {
                    clientBuilder.addInterceptor(interceptor)
                }
            }
            if (proxy != null) {
                this.setProxy(clientBuilder, proxy)
            }

            return clientBuilder.build()
        }

        fun setProxy(clientBuilder: OkHttpClient.Builder, proxy: Proxy): OkHttpClient.Builder {
            // 设置代理
            val p = if (proxy.proxyType == ProxyType.direct) java.net.Proxy(
                typeMapping[proxy.protocol], InetSocketAddress(
                    InetAddress.getByName(proxy.ip), proxy.port.toInt()
                )
            ) else {
                // 隧道
                val tunnel =
                    proxy.tunnels.randomOrNull() ?: throw RuntimeException("创建browser context异常，无法获取隧道列表")
                java.net.Proxy(
                    typeMapping[proxy.protocol], InetSocketAddress(
                        InetAddress.getByName(tunnel.localIp.trim()), tunnel.localPort
                    )
                )
            }

            clientBuilder.proxy(p)

            if (proxy.username != null && proxy.password != null && proxy.username.isNotEmpty()) {
                clientBuilder.proxyAuthenticator(Authenticator { route, response ->
                    val credential =
                        "Basic " + Base64.encode("${proxy.username.trim()}:${proxy.password.trim()}".toByteArray())
                    return@Authenticator response.request.newBuilder()
                        .header("Proxy-Authorization", credential)
                        .build()
                })
            }

            return clientBuilder
        }

        fun getRequestBuilder(url: String, ua: String): Request.Builder {
            val requestBuilder = Request.Builder().header("User-Agent", ua)
            return requestBuilder.url(url)
        }
    }

    fun getCookie(domain: String, name: String): String? =
        (this.client.cookieJar as CookieJarManager).getCookie(domain, name)

    fun getCookie(domains: List<String>, name: String): String? {
        for (domain in domains) {
            val cookie = (this.client.cookieJar as CookieJarManager).getCookie(domain, name)
            if (cookie != null) {
                return cookie
            }
        }
        return null
    }

    fun getCookies(domain: String): Map<String, String> =
        (this.client.cookieJar as CookieJarManager).getCookies(domain)

    inline fun <T> runSafely(
        request: Request,
        onException: (e: Throwable) -> T = { e -> throw e },
        onSuccess: (r: Response) -> T
    ): T {
        return try {
            this.client.newCall(request).execute().use { r ->
                onSuccess(r)
            }
        } catch (e: Exception) {
            logger.error("http请求错误: ${request.url}", e)
            onException(e)
        }
    }
}

fun Request.Builder.accept(type: String) = this.header("Accept", type)
fun Request.Builder.acceptAll() = this.header("Accept", "*/*")
fun Request.Builder.acceptJson() = this.header("Accept", "application/json")
fun Request.Builder.acceptEncodingGzip() = this.header("Accept-Encoding", "gzip, deflate, br")
fun Request.Builder.acceptLang(lang: String) = this.header("Accept-Language", lang)
fun Request.Builder.acceptEnLang() = this.header("Accept-Language", "en")
fun Request.Builder.acceptZHLang() = this.header("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7")
fun Request.Builder.host(host: String) = this.header("Host", host)
fun Request.Builder.origin(origin: String) = this.header("Origin", origin)
fun Request.Builder.contentTypeJson() = this.header("Content-Type", "application/json")
fun Request.Builder.contentTypeJsonUtf8() = this.header("Content-Type", "application/json;charset=UTF-8")
fun Request.Builder.contentFormUrlEncoded() =
    this.header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")

fun Request.Builder.referer(referer: String) = this.header("Referer", referer)
fun Request.Builder.secFetchDest(dest: String) = this.header("Sec-Fetch-Dest", dest)
fun Request.Builder.secFetchMode(mode: String) = this.header("Sec-Fetch-Mode", mode)
fun Request.Builder.secFetchSite(site: String) = this.header("Sec-Fetch-Site", site)
fun Request.Builder.secFetchUser(user: String) = this.header("Sec-Fetch-User", user)
fun Request.Builder.connectionKeepAlive() = this.header("Connection", "keep-alive")
fun Request.Builder.basicAuthorization(token: String) = this.header("Authorization", "Basic " + Base64.encode(token))
fun Request.Builder.bearerAuthorization(token: String) = this.header("Authorization", "Bearer " + token)